
import java.io.*;
import java.net.*;
import java.util.*;

import javax.swing.Icon;

/**
 * An encapsulation of a worker thread that sends a query string to a 
 * single search engine web site and processes the results.
 */
abstract class SearchWorker extends SwingWorker
{
    private final Search search;
    private final HashMap bindingsMap = new HashMap();
    private final String query;
    private ArrayList buffer = new ArrayList();
    private int nHits = 0;
    private int maxHits = 200;
    private boolean updateNeeded = false;
    private long readIdleTime = 0;


    /**
     * [TBD javadoc.  search can be null]
     */
    public SearchWorker(Search search, String query) {
	this.search = search;
	this.query = query;
    }


    /** 
     * @return the query we'll send to the search enginege 
     */
    public String getQuery() {
	return query;
    }


    /**
     * Maximum number of hits we'll download.
     * @return the value of the maxHits property.
     * @see #setMaxHits
     */
    public int getMaxHits() {
	return maxHits;
    }


    /**
     * After maxHits have been downloaded, stop.  The default
     * value for this property is 200.
     * @see #getMaxHits
     */
    public void setMaxHits(int n) {
	maxHits = n;
    }


    /**
     * Number of hits we've downloaded so far.
     * @return the value of the nHits property.
     * @see #setMaxHits
     */
    public int getNHits() {
	return nHits;
    }


    /**
     * Return the PageParser that can be used to extract the search hits
     * from one (HTML) page of results.
     */
    abstract protected PageParser getPageParser();


    /**
     * Given a simple search query q, return the URL for the page of 
     * search hits that begins with hit number t.
     */
    abstract protected URL getPageURL(String q, int t) throws MalformedURLException;


    /** 
     * Quietly close the reader, ignoring IOExceptions.
     */
    private void closeReader(Reader r) {
	if (r != null) {
	    try {
		r.close();
	    }
	    catch (IOException ignore) {
	    }
	}
    }


    /** 
     * Load and parse the contents of one web pages search hits in the 
     * buffer List, beginning with hit (number) t.  Return false if the
     * page is empty or an error occurs while trying to read it.  Otherwise
     * return true.
     * 
     * @see #getPageParser
     * @see #getPageURL
     */
    private boolean loadPage() {
	PageParser parser = getPageParser();
	boolean emptyPage = true;
	Reader inputReader = null;
	try {
	    InputStream ist = getPageURL(getQuery(), getNHits()).openStream();

	    /* This InputStreamReader subclass reader measures how long we've spent 
	     * idled by a blocked read call.  See getReadIdleTime();
	     */
	    inputReader = new InputStreamReader(ist) {
		public int read(char cbuf[], int off, int len) throws IOException {
		    long t0 = System.currentTimeMillis();
		    int rv = super.read(cbuf, off, len);
		    long t1 = System.currentTimeMillis();

		    /* Don't bother with deltas less than 3 ms since they can be
		     * accounted for by clock sampling errors.  If this thread is 
		     * blocked (and likely unscheduled) then the delay will be 
		     * substantially longer.
		     */
		    if (t1 > (t0 + 2)) {
			readIdleTime += t1 - t0;
		    }

		    return rv;
		}
	    };

	    while((nHits < maxHits) && parser.parse(inputReader, bindingsMap)) {
		buffer.add(new SearchHit(this, bindingsMap));
		bindingsMap.clear();
		nHits += 1;
		emptyPage = false;
		maybeUpdateSearch();
	    }
	}
	catch (Exception e) {
	    return false;
	}
	finally {
	    closeReader(inputReader);
	}
	return !emptyPage;
    }


    /** 
     * Load up to maxHits hits for query, return nHits.
     */
    public Object construct() {
	while(loadPage()) 
	    ;
	updateSearch(false); 
	return new Integer(getNHits());
    }


    /** 
     * Let the Search object know that we're finished.  This method is not called 
     * if the worker is interrupted or if an uncaught exception causes it to 
     * abort.  Note also that this method is called on the event dispatching thread.
     */
    public void finished() {
	if (search != null) {
	    search.finished(this);
	}
    }



    /** 
     * Return the total time, in milliseconds, that we've spent waiting for the 
     * read() method to return.  This includes the time when the thread isn't
     * running, typically because it's waiting to be scheduled after the blocking
     * on a read call.
     */
    public long getReadIdleTime() {
	return readIdleTime;
    }


    /** 
     * If true, advise the worker that at the next opportunity it 
     * should deliver as many SearchHits as it's buffered up to 
     * the Search object. The Search.update() method will be called 
     * on the event dispatching thread.
     * <p>
     * This is SearchWorker method than can be called from 
     * other threads, notably the event dispatching thread.
     */
    public synchronized void setUpdateNeeded(boolean f) {
	updateNeeded = f;
    }


    /** 
     * @return the value of the updateNeeded property.
     * @see #setUpdateNeeded
     */
     public synchronized boolean isUpdateNeeded() {
	return updateNeeded;
    }


    /** 
     * If updateNeeded is true and new SearchHits are available, clear 
     * updateNeeded and then call updateSearch().
     */
    private synchronized void maybeUpdateSearch() {
	if (isUpdateNeeded() && (buffer.size() > 0)) {
	    setUpdateNeeded(false);
	    updateSearch(true);
	}
    }


    /** 
     * Call search.update() on the event dispatching thread.  This method 
     * delivers the current SearchHit buffer to the update method and resets 
     * buffer to a new (empty) List.  The resetBuffer parameter should be
     * true, unless we've finished loading SearchHits.
     */
    private synchronized void updateSearch(boolean resetBuffer) {
	if (search != null) {
	    final List newResults = buffer;
	    buffer = (resetBuffer) ? new ArrayList(buffer.size()) : null;
	    Runnable doUpdateSearch = new Runnable() {
		public void run() {
		    search.update(newResults);
		}
	    };
	    java.awt.EventQueue.invokeLater(doUpdateSearch);
	}
    }


    /** 
     * Return the name of the search engine we're using, e.g. 
     * "Yahoo" or "Excite".
     */
    abstract public String getSiteName();


    /** 
     * Return an icon that represents the search engine we're using
     */
    abstract public Icon getSiteIcon();


    public String toString() {
	String status = (getValue() != null) ? "finished" : "working";
	return getSiteName() + ": " + getNHits() + " hits [" + status + "]";
    }
}
